2024年3月13日
By: Chase

Let's Talk About React's New Features

Introduction

In a recent interview, we talked about the major React updates in recent years, and I realized that my in-depth understanding was still at the time when Hooks were first introduced. My recent attention to the community has only been at the level of knowing that 18.2 is out and that it has fully embraced Next.js.

More anxiety-inducing than age for programmers is the lack of understanding of new changes.

This article is not intended to provide a detailed explanation of the new hooks. The official documentation is definitely the most detailed for that. Instead, it's mainly to write some demos to help myself understand.

Import React 18.2 in the Blog


        console.log('React version:', React.version)
        console.log('ReactDOM version:', ReactDOM.version)
    

useLayoutEffect

This is a feature that came out in version 17.x, but I haven't used it yet.

The official description: useLayoutEffect is a version of useEffect that fires before the browser repaints the screen.

To improve that useLayoutEffect has a blocking effect on rendering, I wrote the following simple demo:

Clicking inside the red box loads a small blue box, and clicking the green box hides the small blue box.

To see the difference, I added a heavy loop, switch to click several times.

When you change the example code to useEffect, you can clearly see a flicker.

const { createRoot } = ReactDOM
const { useEffect, useLayoutEffect, useState } = React

const root = createRoot(document.getElementById('useLayoutEffect'))

const ChildDom = ({ position }) => {
    const [state, setState] = useState({ x: 0, y: 0 })

    // !!!!change this to useEffect to see the difference!!!!
    useLayoutEffect(() => {
        for (let i = 0; i < 1e8; i += 1) {
            const a = Math.random()
        }
        setState(position)
    }, [])

    return (
        <div style={{
            position: 'absolute',
            left: state.x,
            top: state.y,
            width: '100px',
            height: '100px',
            background: 'blue'
        }}>
            child
        </div>
    )
}

const App = () => {
	const [childPosition, setChildPosition] = useState(null)

    return (
		<div
            style={{
                display: 'flex'
            }}
        >
            <div
                onClick={e => {
                    setChildPosition({
                        x: e.nativeEvent.offsetX,
                        y: e.nativeEvent.offsetY
                    })
                }}
                style={{
                    border: '1px solid red',
                    width: '200px',
                    height: '200px',
                    position: 'relative'
                }}
            >
                click to show area
                
                {childPosition && (
                    <ChildDom
                        position={childPosition}
                    />
                )}
            </div>

            <div
                onClick={() => setChildPosition(null)}
                style={{
                    border: '1px solid green',
                    width: '200px',
                    height: '200px',
                    position: 'relative'
                }}
            >
                click to disappear area
            </div>
        </div>
    )
}

root.render(<App />);

There's a reason why I know but haven't used this feature yet, as I can't immediately think of a use case for it. For example, in the demo I wrote above, the initial state set value inside the component can be completely avoided in another way.

The Highlights of React 18

The most talked about feature in React 18 is concurrent rendering, which addresses what issue? Let's look at a demo first. Try switching between the two buttons, is it laggy?

It's laggy because activeB renders 100,000 div.

The official documentation provides two hooks, useTransition and useDeferredValue.

The useTransition allows for concurrent UI events at the same level as rendering activeB div, you could set a pending UI to achieve a smoother interaction.

The useDeferredValue defers the rendering of activeB, allowing it to be interrupted, also improving the user interaction experience.

const { createRoot } = ReactDOM
const { useState } = React

const root = createRoot(document.getElementById('demo'))

const ChildA = () => {
    console.log('renderA')

    return (
        <div>
            childA
        </div>
    )
}

const ChildB = () => {
    console.log('render heavy B')

    return (
        <div>
            {new Array(1e5).fill(0).map((_, i) => (
                <div key={i}>
                    childB
                </div>
            ))}
        </div>
    )
}

const App = () => {
    const [activeA, setActiveA] = useState(true)

    const handleClickButton = (param) => {
        setActiveA(param)
    }

    return (
        <div>
            <button
                style={{ color: activeA ? 'red' : 'black' }}
                onClick={() => handleClickButton(true)}
            >
                activate A
            </button>
            <button
                style={{ color: !activeA ? 'red' : 'black' }}
                onClick={() => handleClickButton(false)}
            >
                activate B
            </button>

            {activeA ? <ChildA /> : <ChildB />}
        </div>
    )
}

root.render(<App />);

useTransition

In this example, after clicking activeB, a pending UI is "concurrently" rendered (you can also change it to a pre-activated state of the activeB button) until ChildB is fully rendered. From a UI interaction perspective, the lagging sensation is significantly reduced.

const { createRoot } = ReactDOM
const { useTransition, useState } = React

const root = createRoot(document.getElementById('demo-useTransition'))

const ChildA = () => {
    console.log('renderA')

    return (
        <div>
            childA
        </div>
    )
}

const ChildB = () => {
    console.log('render heavy B')

    return (
        <div>
            {new Array(1e5).fill(0).map((_, i) => (
                <div key={i}>
                    childB
                </div>
            ))}
        </div>
    )
}

const App = () => {
    const [activeA, setActiveA] = useState(true)
    const [isPending, startTransition] = useTransition();

    const handleClickButton = (param) => {
        startTransition(() => {
            setActiveA(param)
        })
    }

    return (
        <div>
            <button
                style={{ color: activeA ? 'red' : 'black' }}
                onClick={() => handleClickButton(true)}
            >
                activate A
            </button>
            <button
                style={{ color: !activeA ? 'red' : 'black' }}
                onClick={() => handleClickButton(false)}
            >
                activate B
            </button>

            {isPending ? <div>loading...</div>
            : activeA ? <ChildA /> : <ChildB />}
        </div>
    )
}

root.render(<App />);

useDeferredValue

In this example, I separated the state controlling the activation of the activeB button and the state rendering activeB list. The two states trigger "concurrently", I placed the heavier renderListA in useDeferredValue.

You can see in the example that the activeB button is activated immediately, but the list rendering is delayed.

This isn't something that can be done by simply splitting the state into two. You can try changing the renderListA in the example to const renderListA = activeA, it won't work as expected.

const { createRoot } = ReactDOM
const { useDeferredValue, useState, useMemo } = React

const root = createRoot(document.getElementById('demo-useDeferredValue'))

const ChildA = () => {
    console.log('renderA')

    return (
        <div>
            childA
        </div>
    )
}

const ChildB = () => {
    console.log('render heavy B')

    return (
        <div>
            {new Array(1e5).fill(0).map((_, i) => (
                <div key={i}>
                    childB
                </div>
            ))}
        </div>
    )
}

const App = () => {
    const [activeA, setActiveA] = useState(true)
    const renderListA = useDeferredValue(activeA)

    return (
        <div>
            <button
                style={{ color: activeA ? 'red' : 'black' }}
                onClick={() => setActiveA(true)}
            >
                activate A
            </button>
            <button
                style={{ color: !activeA ? 'red' : 'black' }}
                onClick={() => setActiveA(false)}
            >
                activate B
            </button>

            {renderListA ? <ChildA /> : <ChildB />}
        </div>
    )
}

root.render(<App />);
Tags: useLayoutEffect useTransition React useDeferredValue